import java.net.ServerSocket;
import java.net.Socket;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.PrintStream;
import java.io.File;
import java.io.FileReader;
import java.util.StringTokenizer;
import javax.naming.NamingException;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.BasicAttribute;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Date;

//The web / mail server 
public class Server{

	static Vector<Mail> mails = new Vector<Mail>();
	private static Object lock = new Object();
	
	public static void main(String[] args){
		System.out.println("Creating Server socket");
		ServerSocket ss = null;
		//try to create socket
		try{
			ss = new ServerSocket(8080);
		}catch(IOException e){
			System.err.println("Could not create socket: "+e);
			System.exit(1);
		}
		new Sender().start();
		//inf-loop and wait for connections from clients
		for(;;){
			System.out.println("Waiting for client...");
			try{
				//if successful create new thread for the socket
				(new Worker(ss.accept())).start();
			}catch(IOException e){
				System.err.println(e);
			}
			System.out.println("Client connected");
		}
	}
	
	//the thread class that handles connections from clients
	private static class Worker extends Thread {
		Socket socket;
		BufferedReader request;
		PrintStream response;
		
		public Worker(Socket socket){
			this.socket = socket; //save the socket
			
		}
		
		public void run() {
			String msg = ""; //holds the string recv:d from client
			try{
				response = new PrintStream(socket.getOutputStream());
				request = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			}catch(IOException e){
				error("failed to create reader/writer to socket",socket);
				System.err.println(e);
				return;
			}

			try{
				msg = request.readLine(); 
				System.err.println(msg);
			}catch(IOException e){
				System.err.println(e);
			}
			if(msg == null || msg.length() == 0){ //if the message is unitialized or 0 
				System.err.println("msg is empty?");
				error("we got a empty message",socket);
				return;
			}
			StringTokenizer tokens = new StringTokenizer(msg," "); //tokenize the message with space as delimeter
			String type = tokens.nextToken(); // GET or POST
			if(type.equalsIgnoreCase("GET")){ //GET case
				System.err.println("GOT A GET");
				String requested = tokens.nextToken(); //get the requested document
				if(requested.charAt(0) != '/'){ //don't care about the '/'. The request looks something like this: "GET /document 1.1"
					error("did not get a '/' after GET in header",socket);
					return;
				}
				requested = requested.substring(1); //the filename of the requested document
				File requestedDocument = new File(requested);
				if(!requestedDocument.isFile() || !requestedDocument.canRead()){ //check if there is a readable file in the filesystem with the same name as document
					msg = "";
					synchronized(lock){
						for(int i = 0; i < mails.size(); ++i){
							if(mails.get(i).error){
								msg += "subject - " + mails.get(i).subject + "; From: " + mails.get(i).from + "; To: " + mails.get(i).to + "; error: got a error while sending mail<br />";
								mails.remove(i);
								--i;
							}else{
								msg += "subject - " + mails.get(i).subject + "; From: " + mails.get(i).from + "; To: " + mails.get(i).to + "; added: " + new Date(mails.get(i).sub).toString() + "; delay: " + new Date(mails.get(i).time*1000).toString() + "<br />";
							}
						}
					}
					error(msg,socket);
					return;
				}
				System.err.println("File found");
				try{ //close input on the socket, because we have recvd the data we want at this point 
					socket.shutdownInput();
				}catch(IOException e){
					error("failed to shutdown socket input",socket);
					return;
				}
				//Send header to client
				response.println("HTTP/1.1 200 OK");
				response.println("Server: craptastic");
				response.println("Content-Type: text/html; charset=ISO-8859-15");
				response.println();
				response.println(getContents(requestedDocument)); //send the contents of the requested document
				response.println();
				try{ //close the socket
					//socket.shutdownOutput();
					socket.close();
				}catch(IOException e){
					error("failed to shutdown socket",socket);
					return;
				}
			}else if(type.equalsIgnoreCase("POST")){ //POST case
				System.err.println("Got a POST");
				msg = ""; //stores the msg recv:d from client
				int length = 0; //stores the length of the body of the document
				try{ //try to read the HTTP-header from the socket 
					while((msg = request.readLine()) != null && msg.length() > 0){
						System.err.println(msg);
						tokens = new StringTokenizer(msg," ");
						type = tokens.nextToken(); // content-length
						if(type.equals("Content-Length:"))
							try{
								length = Integer.parseInt(tokens.nextToken()); //save the content-length (length of body)
							}catch(NumberFormatException e){
								System.err.println("length is not a integer");
								error("length is not a integer",socket);
								return;
							}
					}
				}catch(IOException e){ //couldn't read the header 
					System.err.println("couldnt read header");
					error("couldnt read header",socket);
					return;
				} 
				if(length == 0){ //the content-length was 0
					error("malformed body from client",socket);
					return;
				}
				System.err.println(length);
				System.err.println("reading done");
				char body[] = new char[length]; //will hold the body
				try{ //try to read the body from the socket to the body-buffer
					request.read(body, 0, length);
				}catch(IOException e){ //failed to read body
					System.err.println("couldnt read body");
					error("couldnt read body",socket);
					return;
				}
				msg = new String(body); //create a string from the body-buffer
				System.err.println(msg);
				tokens = new StringTokenizer(msg,"&"); //split the body on ampersands to get the parameters sent
				if(tokens.countTokens() != 6){
					System.err.println("incorrect number of params");
					error("incorrect number of params",socket);
					return;
				} 
				String from = tokens.nextToken().trim(); //get the from-field (sender) 
				String to = tokens.nextToken().trim();   //get the to-field (recipient)
				String server = tokens.nextToken().trim(); //url/ip of mx-server
				String subject = tokens.nextToken().trim(); //ge the subject of the mail
				String time = tokens.nextToken().trim(); //ge the time of when the mail is to be sent
				String content = tokens.nextToken().trim(); //the content of the mail to send
				try{ //url-decode the fields
					from = java.net.URLDecoder.decode(from.substring(from.indexOf('=')+1), "ISO-8859-15");
					to = java.net.URLDecoder.decode(to.substring(to.indexOf('=')+1), "ISO-8859-15");
					server = java.net.URLDecoder.decode(server.substring(server.indexOf('=')+1), "ISO-8859-15");
					subject = java.net.URLDecoder.decode(subject.substring(subject.indexOf('=')+1), "ISO-8859-15");
					time = java.net.URLDecoder.decode(time.substring(time.indexOf('=')+1), "ISO-8859-15");
					content = java.net.URLDecoder.decode(content.substring(content.indexOf('=')+1), "ISO-8859-15");
					String newContent = "";					
					for(int start = 0; start < content.length(); start += 20){
						int end = Math.min(start + 20, content.length());
						newContent += content.substring(start, end).replace("=","=3D").replace("\t","=09").replace(" ","=20").replace(".","=2E").replace("å","=E5").replace("ä","=E4").replace("ö","=F6").replace("Å","=C5").replace("Ä","=C4").replace("Ö","=D6") + "=\n";
					}
				content = newContent;
				}catch(Exception e){ //failed to decode
					e.printStackTrace();
					error("failed to decode/encode params",socket);
					return;
				}
				if(server.trim().length() == 0){
					tokens = new StringTokenizer(to,"@"); //split the mail to get the address
					tokens.nextToken();
					String addr = tokens.nextToken();
					try{
						server = getMX(addr);
						if(server == null){
							System.out.println("Could not get MX-server");
							error("Could not get MX-server",socket);
							return;
						}
						System.out.println(server);
					}catch(NamingException e){
						System.out.println("NAMING EXCEPTION");
						error("NAMING EXCEPTION",socket);
						return;
					}
					
				}else if(!server.matches("([a-zA-Z0-9_åäö-]+(.[a-zA-Z0-9_åäö-]+)*\\.[a-zA-Z]{2,4}|[0-9]{1,3}(.[0-9]{1,3}){3,3})")){ //check so server got proper syntax
					System.err.println("regex didnt match in server");
					error("regex didnt match in server",socket);
					return;
				}
				
						
				System.err.println(from);
				System.err.println(to);
				System.err.println(server);
				System.err.println(subject);
				System.err.println(content);
				//regex to check if the form/to-fields contains correct mail addresses
				if(!from.matches("[a-zA-Z0-9åäö._-]+@[a-zA-Z0-9åäö-]+(.[a-zA-Z0-9åäö-]+)*\\.[a-zA-Z]{2,4}")){
					System.err.println("regex didnt match in from");
					error("regex didnt match in from",socket);
					return;
				}
				if(!to.matches("[a-zA-Z0-9åäö._-]+@[a-zA-Z0-9åäö-]+(.[a-zA-Z0-9åäö-]+)*\\.[a-zA-Z]{2,4}")){
					System.err.println("regex didnt match in to");
					error("regex didnt match in to",socket);
					return;
				}
				
				if(!time.equals("")){
					long nTime = 0;
					try{
						nTime = System.currentTimeMillis()/1000+Integer.parseInt(time);
					}catch(NumberFormatException e){
						System.err.println("time is not a integer");
						error("time is not a integer",socket);
						return;
					}
					synchronized(lock){
						mails.add(new Mail(from, to, server, subject, nTime, content));
					}
					error("successfully stored message", socket);
					return;
				}else{
					if((msg = sendMail(new Mail(from, to, server, subject, System.currentTimeMillis()/1000, content))) == null){
						error("success!",socket);
					}else{
						error(msg, socket);
					}
				}
				try{
					socket.close();
				}catch(IOException e){}
			}	
		}		
	}
	
	/*
	* This is the Thread that checks when a delayed mail should be sent, and send it if it is time for it to be sent
	*/
	private static class Sender extends Thread {
	
		public Sender(){}
		
		public void run() {
			while(true){
				synchronized(lock){
					for(int i = 0; i < mails.size(); ++i){
						if(mails.get(i).time < System.currentTimeMillis()/1000 && !mails.get(i).error){
							StringTokenizer tokens = new StringTokenizer(mails.get(i).from,"@"); //split the mail to get the address
							tokens.nextToken();
							String addr = tokens.nextToken();
							try{
								if(sendMail(mails.get(i)) != null){ //recevies null when successfully sent a mail, if not null then we got a error message
									mails.get(i).error = true;
									sendMail(new Mail("gushed@kth.se", mails.get(i).from, getMX(addr), "Failed to send mail", 0l,  "Failed=20to=20send=20your=20mail=20with=20subject:=20'" + mails.get(i).subject + "'"));
								}else{
									sendMail(new Mail("gushed@kth.se", mails.get(i).from, getMX(addr), "Successfully sent mail", 0l, "Successfully=20sent=20your=20mail=20with=20subject:=20'" + mails.get(i).subject + "'"));
									mails.remove(i);
									--i;
								}
							}catch(NamingException e){
								System.err.println("could not find MX server when trying to notify sender");
							}
						}
					}
				}
				try{
					sleep(1000);
				}catch(java.lang.InterruptedException e){
				
				}
			}
		}	
	}
	
	/*
	* The mail class. Contains all the information needed for a mail.
	*/
	private static class Mail {
		public String from;
		public String to;
		public String server;
		public String subject;
		public long time;
		public String content;
		public boolean error;
		public long sub;
		
		public Mail(String from, String to, String server, String subject, long time, String content){
			this.from = from;
			this.to = to;
			this.server = server;
			this.subject = subject;
			this.time = time;
			this.content = content;
			this.error = false;
			this.sub = System.currentTimeMillis();
		}
	
	}
	
	/*
	* Sends the specified mail. All information needed should already be in the mail
	*/
	public static String sendMail(Mail mail){
		//Send the mail to the smtp-server.	
		String msg = "";
		String to = mail.to;
		String from = mail.from;
		String server = mail.server;
		String subject = mail.subject;
		long time = mail.time;
		String content = mail.content; 
		System.err.println(server);
		
		try{
			Socket smtpSocket = new Socket(server, 25); 
			PrintStream smtpRequest = new PrintStream(smtpSocket.getOutputStream());
			BufferedReader smtpResponse = new BufferedReader(new InputStreamReader(smtpSocket.getInputStream()));
			
			//connect to server
			//recevie response on the server
			System.out.println((msg = smtpResponse.readLine()));
			if(!getResponseCode(msg).equals("220")){
				System.err.println("START err");
				return "START err";
			}
			smtpRequest.println("HELO Craptastic");
			System.out.println("HELO Craptastic");
			System.out.println((msg = smtpResponse.readLine()));
			if(!getResponseCode(msg).equals("250")){
				System.err.println("HELO err");
				return "HELO err";
			}
			smtpRequest.println("MAIL FROM:<" + from + ">");
			System.out.println("MAIL FROM:<" + from + ">");			
			System.out.println((msg = smtpResponse.readLine()));
			if(!getResponseCode(msg).equals("250")){
				System.err.println("MAIL FROM err");
				return "MAIL FROM err";
			}
			smtpRequest.println("RCPT TO:<" + to + ">");
			System.out.println("RCPT TO:<" + to + ">");			
			System.out.println((msg = smtpResponse.readLine()));
			if(!getResponseCode(msg).equals("250")){
				System.err.println("RCPT TO err");
				return "RCPT TO err";
			}
			smtpRequest.println("DATA");
			System.out.println("DATA");
			System.out.println((msg = smtpResponse.readLine()));
			if(!getResponseCode(msg).equals("354")){
				System.err.println("DATA err");
				return "DATA err";
			}
			smtpRequest.println("From: <" + from + ">");
			System.out.println("From: <" + from + ">");
			smtpRequest.println("To: <" + to + ">");
			System.out.println("To: <" + to + ">");
			smtpRequest.println("Subject: " + subject);
			System.out.println("Subject: " + subject);
			smtpRequest.println("MIME-Version: 1.0\nContent-Type: text/plain; charset=ISO-8859-15\nContent-Transfer-Encoding: quoted-printable");
			System.out.println("MIME-Version: 1.0\nContent-Type: text/plain; charset=ISO-8859-15\nContent-Transfer-Encoding: quoted-printable");
			smtpRequest.println();
			smtpRequest.print(content);
			System.out.print(content);
			smtpRequest.print("\r\n.\r\n"); //end the DATA field
			System.out.print("\r\n.\r\n");
			System.out.println((msg = smtpResponse.readLine()));
			if(!getResponseCode(msg).equals("250")){
				System.err.println("queued err");
				return "queued err";
			}
			smtpRequest.println("QUIT");
			System.out.println("QUIT");
			System.out.println((msg = smtpResponse.readLine()));
			if(!getResponseCode(msg).equals("221")){
				System.err.println("QUIT err");
				return "QUIT err";
			}					
			smtpSocket.close(); //shutdown socket
		}catch(IOException e){
			System.err.println("error from smtp-server");
			return "error from smtp-server";
		}

		return null;
	}
	
	/* 
	 * Use to send error messages upon errors
	 */
	static public void error(String error, Socket socket){
		try{
			PrintStream response = new PrintStream(socket.getOutputStream());
			response.println("HTTP/1.1 200 OK");
			response.println("Server: craptastic");
			response.println("Content-Type: text/html; charset=ISO-8859-15");
			response.println();
			response.println("<html><head><title>Status</title></head><body><p>" + error + "</p><br /><a href=\"http://localhost:8080/mail.html\">Back</a> </body></html>");
			response.println();
		}catch(IOException e){
			e.printStackTrace();
		}
		try{
			socket.shutdownOutput();
			socket.close();
		}catch(IOException e){
			System.err.println(e);
		}
	}

	/*
	 * Get the contents of a file, and return it in a String.
	 */	
	static String getContents(File file) {
		StringBuilder contents = new StringBuilder();		
		try {
			BufferedReader input =	new BufferedReader(new FileReader(file));
			try {
				String line = "";
				while ((line = input.readLine()) != null){
					contents.append(line);
					contents.append(System.getProperty("line.separator"));
				}
			}finally {
				input.close();
			}
		}
		catch (IOException e){
			e.printStackTrace();
		}		
		return contents.toString();
	}
	
	/*
	*	Returns the response code at the start of the message. Used to get the codes used in the smtp-protocol
	*/
	static String getResponseCode(String msg){
		StringTokenizer tokens = new StringTokenizer(msg," ");
		if(tokens.hasMoreTokens())
			return tokens.nextToken().trim();
		return "0";
	}

	/*
	* Get the MX-server from a host. It will return one if one is found, null otherwise.
	*/
	static String getMX(String host) throws NamingException {
		Hashtable<String,String> env = new Hashtable<String,String>();
		env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
		InitialDirContext ictx = new InitialDirContext(env);
		BasicAttributes attrs = (BasicAttributes)ictx.getAttributes(host, new String[] {"MX"});
		BasicAttribute attr = (BasicAttribute)attrs.get("MX");
		if(attr == null) 
			return null;
		StringTokenizer tokens = new StringTokenizer(attr.get().toString()," ");
		if(tokens.countTokens() < 2)
			return null;
		tokens.nextToken(); //this is a number, not relevant as we want the address
		return tokens.nextToken().trim();
  	}	
}
